/*
  Copyright 2020 the JSDoc Authors.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      https://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

import * as type from '../../../lib/type.js';

describe('@jsdoc/tag/lib/type', () => {
  function buildText(typeInfo, name, desc) {
    let text = '';

    if (typeInfo) {
      text += `{${typeInfo}}`;
      if (name || desc) {
        text += ' ';
      }
    }

    if (name) {
      text += name;
      if (desc) {
        text += ' ';
      }
    }

    if (desc) {
      text += desc;
    }

    return text;
  }

  it('is an object', () => {
    expect(type).toBeObject();
  });

  it('exports a parse function', () => {
    expect(type.parse).toBeFunction();
  });

  describe('parse', () => {
    it('returns an object with name, type, and text properties', () => {
      const info = type.parse('');

      expect(info.name).toBeString();
      expect(info.type).toBeArray();
      expect(info.text).toBeString();
    });

    it('does not extract a name or type if canHaveName and canHaveType are not set', () => {
      const desc = '{number} foo The foo parameter.';
      const info = type.parse(desc);

      expect(info.type).toBeEmptyArray();
      expect(info.name).toBe('');
      expect(info.text).toBe(desc);
    });

    it('extracts a name, but not a type, if canHaveName is true and canHaveType is false', () => {
      const name = 'bar';
      const desc = 'The bar parameter.';
      const info = type.parse(buildText(null, name, desc), true, false);

      expect(info.type).toBeEmptyArray();
      expect(info.name).toBe(name);
      expect(info.text).toBe(desc);
    });

    it('extracts a type, but not a name, if canHaveName is false and canHaveType is true', () => {
      const typeString = 'boolean';
      const desc = 'Set to true on alternate Thursdays.';
      const info = type.parse(buildText(typeString, null, desc), false, true);

      expect(info.type).toEqual([typeString]);
      expect(info.name).toBe('');
      expect(info.text).toBe(desc);
    });

    it('extracts a name and type if canHaveName and canHaveType are true', () => {
      const typeString = 'string';
      const name = 'baz';
      const desc = 'The baz parameter.';
      const info = type.parse(buildText(typeString, name, desc), true, true);

      expect(info.type).toEqual([typeString]);
      expect(info.name).toBe(name);
      expect(info.text).toBe(desc);
    });

    it('reports optional types correctly for both JSDoc and Closure syntax', () => {
      let desc = '{string} [foo]';
      let info = type.parse(desc, true, true);

      expect(info.optional).toBeTrue();

      desc = '{string=} [foo]';
      info = type.parse(desc, true, true);
      expect(info.optional).toBeTrue();

      desc = '[foo]';
      info = type.parse(desc, true, true);
      expect(info.optional).toBeTrue();
    });

    it('returnsthe types as an array', () => {
      const desc = '{string} foo';
      const info = type.parse(desc, true, true);

      expect(info.type).toEqual(['string']);
    });

    it('recognizes the entire list of possible types', () => {
      let desc = '{(string|number)} foo';
      let info = type.parse(desc, true, true);

      expect(info.type).toEqual(['string', 'number']);

      desc = '{ ( string | number ) } foo';
      info = type.parse(desc, true, true);
      expect(info.type).toEqual(['string', 'number']);

      desc = '{  (   string  | number)} foo';
      info = type.parse(desc, true, true);
      expect(info.type).toEqual(['string', 'number']);

      desc = '{(string|number|boolean|function)} foo';
      info = type.parse(desc, true, true);
      expect(info.type).toEqual(['string', 'number', 'boolean', 'function']);
    });

    it('does not find any type if there is no text in braces', () => {
      const desc = 'braceless text';
      const info = type.parse(desc, false, true);

      expect(info.type).toBeEmptyArray();
    });

    it('copes with bad escapement at the end of the string', () => {
      const desc = 'bad {escapement \\';
      const info = type.parse(desc, false, true);

      expect(info.type).toBeEmptyArray();
      expect(info.text).toBe(desc);
    });

    it('handles escaped braces correctly', () => {
      const desc = '{weirdObject."with\\}AnnoyingProperty"}';
      const info = type.parse(desc, false, true);

      expect(info.type[0]).toBe('weirdObject."with}AnnoyingProperty"');
    });

    it('works if the type expression is the entire string', () => {
      const desc = '{textInBraces}';
      const info = type.parse(desc, false, true);

      expect(info.type[0]).toBe('textInBraces');
    });

    it('works if the type expression is at the beginning of the string', () => {
      const desc = '{testString} ahoy';
      const info = type.parse(desc, false, true);

      expect(info.type[0]).toBe('testString');
      expect(info.text).toBe('ahoy');
    });

    it('works if the type expression is in the middle of the string', () => {
      const desc = 'a {testString} yay';
      const info = type.parse(desc, false, true);

      expect(info.type[0]).toBe('testString');
      expect(info.text).toBe('a  yay');
    });

    it('works if the tag is at the end of the string', () => {
      const desc = 'a {testString}';
      const info = type.parse(desc, false, true);

      expect(info.type[0]).toBe('testString');
      expect(info.text).toBe('a');
    });

    it('works when there are nested braces', () => {
      const desc = 'some {{double}} braces';
      const info = type.parse(desc, false, true);

      // we currently stringify all record types as 'Object'
      expect(info.type[0]).toBe('Object');
      expect(info.text).toBe('some  braces');
    });

    it('overrides the type expression if an inline @type tag is specified', () => {
      let desc = '{Object} cookie {@type Monster}';
      let info = type.parse(desc, true, true);

      expect(info.type).toEqual(['Monster']);
      expect(info.text).toBe('');

      desc = '{Object} cookie - {@type Monster}';
      info = type.parse(desc, true, true);
      expect(info.type).toEqual(['Monster']);
      expect(info.text).toBe('');

      desc = '{Object} cookie - The cookie parameter. {@type Monster}';
      info = type.parse(desc, true, true);
      expect(info.type).toEqual(['Monster']);
      expect(info.text).toBe('The cookie parameter.');

      desc = '{Object} cookie - The cookie parameter. {@type (Monster|Jar)}';
      info = type.parse(desc, true, true);
      expect(info.type).toEqual(['Monster', 'Jar']);
      expect(info.text).toBe('The cookie parameter.');

      desc = '{Object} cookie - The cookie parameter. {@type (Monster|Jar)} Mmm, cookie.';
      info = type.parse(desc, true, true);
      expect(info.type).toEqual(['Monster', 'Jar']);
      expect(info.text).toBe('The cookie parameter.  Mmm, cookie.');
    });

    describe('JSDoc-style type info', () => {
      it('parses JSDoc-style optional parameters', () => {
        let name = '[qux]';
        const desc = 'The qux parameter.';
        let info = type.parse(buildText(null, name, desc), true, false);

        expect(info.name).toBe('qux');
        expect(info.text).toBe(desc);
        expect(info.optional).toBeTrue();

        name = '[ qux ]';
        info = type.parse(buildText(null, name, desc), true, false);
        expect(info.name).toBe('qux');
        expect(info.text).toBe(desc);
        expect(info.optional).toBeTrue();

        name = '[qux=hooray]';
        info = type.parse(buildText(null, name, desc), true, false);
        expect(info.name).toBe('qux');
        expect(info.text).toBe(desc);
        expect(info.optional).toBeTrue();
        expect(info.defaultvalue).toBe('hooray');

        name = '[  qux   =  hooray ]';
        info = type.parse(buildText(null, name, desc), true, false);
        expect(info.name).toBe('qux');
        expect(info.text).toBe(desc);
        expect(info.optional).toBeTrue();
        expect(info.defaultvalue).toBe('hooray');
      });
    });

    // TODO: Add more tests related to how JSDoc mangles the Catharsis parse results.
    describe('Closure Compiler-style type info', () => {
      it('recognizes variable (repeatable) parameters', () => {
        const desc = '{...string} foo - Foo.';
        const info = type.parse(desc, true, true);

        expect(info.type).toEqual(['string']);
        expect(info.variable).toBeTrue();
      });

      it('sets the type correctly for type applications that contain type unions', () => {
        const desc = '{Array.<(string|number)>} foo - Foo.';
        const info = type.parse(desc, true, true);

        expect(info.type).toEqual(['Array.<(string|number)>']);
      });
    });
  });
});
